package daybook.util.tree;

import java.util.*;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

public final class TreeLoc<T> {
    private final List<Tree<T>> trees;
    private final int index;
    private final TreeLoc<T> parent;

    private TreeLoc(List<Tree<T>> trees, int index, TreeLoc<T> parent) {
        this.trees = trees;
        this.index = index;
        this.parent = parent;
    }

    public Tree<T> getTree() {
        return trees.get(index);
    }

    public Optional<TreeLoc<T>> getParent() {
        return Optional.ofNullable(parent);
    }

    public Optional<TreeLoc<T>> getLeftSibling() {
        if (index <= 0) {
            return Optional.empty();
        } else {
            return Optional.of(new TreeLoc<>(trees, index - 1, parent));
        }
    }

    public Optional<TreeLoc<T>> getRightSibling() {
        if (index >= trees.size() - 1) {
            return Optional.empty();
        } else {
            return Optional.of(new TreeLoc<>(trees, index + 1, parent));
        }
    }

    public TreeLoc<T> getChild(int index) {
        final List<Tree<T>> children = getTree().getChildren();
        if (index < 0 || index >= children.size()) {
            throw new IndexOutOfBoundsException("out of range [0," + children.size() + ")");
        }

        return new TreeLoc<>(children, index, this);
    }

    private static class ParentIterator<T> implements Iterator<Tree<T>> {
        private TreeLoc<T> parent;

        public ParentIterator(TreeLoc<T> parent) {
            this.parent = parent;
        }

        @Override
        public boolean hasNext() {
            return parent != null;
        }

        @Override
        public Tree<T> next() {
            if (parent != null) {
                final Tree<T> tree = parent.getTree();
                parent = parent.parent;
                return tree;
            } else {
                throw new NoSuchElementException();
            }
        }
    }

    public Stream<Tree<T>> parentStream() {
        return StreamSupport.stream(Spliterators.spliteratorUnknownSize(
                new ParentIterator<>(parent),
                Spliterator.ORDERED | Spliterator.IMMUTABLE), false);
    }

    private static class LeftSiblingIterator<T> implements Iterator<Tree<T>> {
        private int index;
        private final List<Tree<T>> trees;

        private LeftSiblingIterator(List<Tree<T>> trees, int index) {
            this.index = index;
            this.trees = trees;
        }

        @Override
        public boolean hasNext() {
            return index > 0;
        }

        @Override
        public Tree<T> next() {
            if (index > 0) {
                return trees.get(--index);
            } else {
                throw new NoSuchElementException();
            }
        }
    }

    public Stream<Tree<T>> siblingsStream() {
        return trees.stream();
    }

    public Stream<Tree<T>> leftSiblingsStream() {
        return StreamSupport.stream(Spliterators.spliteratorUnknownSize(
                new LeftSiblingIterator<>(trees, index),
                Spliterator.ORDERED | Spliterator.IMMUTABLE), false);
    }

    private static class RightSiblingIterator<T> implements Iterator<Tree<T>> {
        private int index;
        private final List<Tree<T>> trees;

        private RightSiblingIterator(List<Tree<T>> trees, int index) {
            this.index = index + 1;
            this.trees = trees;
        }

        @Override
        public boolean hasNext() {
            return index < trees.size();
        }

        @Override
        public Tree<T> next() {
            if (index < trees.size()) {
                return trees.get(index++);
            } else {
                throw new NoSuchElementException();
            }
        }
    }

    public Stream<Tree<T>> rightSiblingsStream() {
        return StreamSupport.stream(Spliterators.spliteratorUnknownSize(
                new RightSiblingIterator<>(trees, index),
                Spliterator.ORDERED | Spliterator.IMMUTABLE), false);
    }

    public static <T> TreeLoc<T> ofRoot(Tree<T>  tree) {
        Objects.requireNonNull(tree);
        return new TreeLoc<>(Collections.singletonList(tree), 0, null);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        TreeLoc<?> treeLoc = (TreeLoc<?>) o;

        if (index != treeLoc.index) return false;
        if (!trees.equals(treeLoc.trees)) return false;
        return parent != null ? parent.equals(treeLoc.parent) : treeLoc.parent == null;

    }

    @Override
    public int hashCode() {
        int result = trees.hashCode();
        result = 31 * result + index;
        result = 31 * result + (parent != null ? parent.hashCode() : 0);
        return result;
    }
}
